package parsing;

import java.util.ArrayList;
import java.util.List;

import scanning.NumberToken;
import scanning.Scanner;
import scanning.Token;
import scanning.TokenType;
import ast.Abbreviation;
import ast.Assignment;
import ast.Body;
import ast.BooleanConstant;
import ast.CharacterConstant;
import ast.Condition;
import ast.Definition;
import ast.Expression;
import ast.Form;
import ast.FunctionCall;
import ast.FunctionDefinition;
import ast.Lambda;
import ast.ListDatum;
import ast.Quote;
import ast.StringConstant;
import ast.Variable;
import ast.VariableDefinition;
import ast.VectorDatum;

public class Parser {

	private Scanner scanner;
	private List<Token> tokenBuffer = new ArrayList<>();
	private int currentIndex = -1;

	public Parser(Scanner scanner) {
		this.scanner = scanner;
		advance();
	}

	public boolean hasNextForm() {
		return !is(TokenType.EndOfFile);
	}

	public Form parseNextForm() {
		return parseForm();
	}

	private void advance() {
		currentIndex++;
		while (currentIndex >= tokenBuffer.size()) {
			Token currentToken = nextNonWhiteSpaceToken();
			tokenBuffer.add(currentToken);
		}
	}

	private Token nextNonWhiteSpaceToken() {
		Token currentToken = scanner.nextToken();
		while (currentToken.getType() == TokenType.WhiteSpace)
			currentToken = scanner.nextToken();
		return currentToken;
	}

	private boolean is(TokenType type) {
		return getCurrentType() == type;
	}

	private TokenType getCurrentType() {
		return getCurrentToken().getType();
	}

	private Token getCurrentToken() {
		return tokenBuffer.get(currentIndex);
	}

	private Form parseForm() {
		if (isExpressionStart())
			return parseExpression();
		else
			return parseDefinition();
	}

	private boolean isExpressionStart() {
		if (!is(TokenType.OpeningBracket))
			return true;
		return !(isLookAheadKeywork("define") || isLookAheadKeywork("define-syntax"));
	}

	private Expression parseExpression() {
		if (is(TokenType.Identifier))
			return parseVariableExpression();
		else if (isLongQuoteStart() || is(TokenType.Quote))
			return parseQuote();
		else if (is(TokenType.OpeningBracket) && isLookAheadKeywork("set!"))
			return parseSet();
		else if (is(TokenType.OpeningBracket) && isLookAheadKeywork("if"))
			return parseIfElse();
		else if (is(TokenType.OpeningBracket) && isLookAheadKeywork("lambda"))
			return parseLambda();
		else if (is(TokenType.OpeningBracket))
			return parseFunctionCall();
		else
			try {
				// TODO remove this try catch and introduce a new isConstant()
				// which is checked here and then throw here if that is not the
				// case either
				return parseConstantExpression();
			} catch (CouldNotParseConstant c) {
				throw new ParserError(c.actualType, "Expression");
			}
	}

	private boolean isLongQuoteStart() {
		return is(TokenType.OpeningBracket) && isLookAheadKeywork("quote");
	}

	private boolean isLookAheadKeywork(String name) {
		Token next = getLookAhead();
		return next.getType() == TokenType.Identifier
				&& next.getText().equalsIgnoreCase(name);
	}

	private Token getLookAhead() {
		while (tokenBuffer.size() <= currentIndex + 1)
			tokenBuffer.add(scanner.nextToken());
		return tokenBuffer.get(currentIndex + 1);
	}

	private Quote parseQuote() {
		boolean isShortForm = is(TokenType.Quote);
		if (isShortForm)
			consume(TokenType.Quote);
		else {
			consume(TokenType.OpeningBracket);
			consumeKeyword("quote");
		}
		Expression datum = parseDatum();
		if (!isShortForm)
			consume(TokenType.ClosingBracket);
		return new Quote(datum);
	}

	private void consumeKeyword(String name) {
		assertIs(TokenType.Identifier);
		if (!name.equalsIgnoreCase(getCurrentText()))
			throw new ParserError(getCurrentType(), "Identifier");
		advance();
	}

	private void assertIs(TokenType tokenType) {
		if (getCurrentType() != tokenType)
			throw new ParserError(getCurrentType(), tokenType.toString());
	}

	@SuppressWarnings("serial")
	public class ParserError extends RuntimeException {

		private String expectedTokenType;
		private TokenType actualTokenType;

		public ParserError(TokenType actualTokenType, String expected) {
			super("expected " + expected + ", but was "
					+ actualTokenType.toString());
			this.expectedTokenType = expected;
			this.actualTokenType = actualTokenType;
		}

		public String getExpectedTokenType() {
			return expectedTokenType;
		}

		public TokenType getActualTokenType() {
			return actualTokenType;
		}
	}

	private Expression parseDatum() {
		Token token = getCurrentToken();
		advance();
		switch (token.getType()) {
		case Boolean:
			return new BooleanConstant(token.getText());
		case Character:
			return new CharacterConstant(token.getText());
		case Number:
			return new NumberParser().parse((NumberToken) token);
		case String:
			return new StringConstant(token.getText());
		case Identifier:
			return new Variable(token.getText());
		case OpeningBracket:
			return parseListDatum();
		case VectorStart:
			return parseVectorDatum();
		default:
			return parseAbbreviation(token.getType());
		}
	}

	private ListDatum parseListDatum() {
		ListDatum list = new ListDatum();
		if (is(TokenType.ClosingBracket)) {
			advance();
			return list;
		}
		parseListContent(list);
		consume(TokenType.ClosingBracket);
		return list;
	}

	private void parseListContent(ListDatum list) {
		while (!is(TokenType.ClosingBracket) && !is(TokenType.Dot))
			list.getData().add(parseDatum());
		if (is(TokenType.Dot)) {
			advance();
			list.setDatumAfterDot(parseDatum());
		}
	}

	private Expression parseVectorDatum() {
		VectorDatum vector = new VectorDatum();
		while (!is(TokenType.ClosingBracket))
			vector.getData().add(parseDatum());
		consume(TokenType.ClosingBracket);
		return vector;
	}

	private Expression parseAbbreviation(TokenType type) {
		Abbreviation.Type abbreviationType;
		switch (type) {
		case Quote:
			abbreviationType = Abbreviation.Type.Quote;
			break;
		case BackQuote:
			abbreviationType = Abbreviation.Type.BackQuote;
			break;
		case Comma:
			abbreviationType = Abbreviation.Type.Comma;
			break;
		case CommaAt:
			abbreviationType = Abbreviation.Type.CommaAt;
			break;
		default:
			throw new ParserError(type, "Datum");
		}
		return new Abbreviation(abbreviationType, parseDatum());
	}

	private Token consume(TokenType tokenType) {
		assertIs(tokenType);
		Token token = getCurrentToken();
		advance();
		return token;
	}

	private Variable parseVariableExpression() {
		Variable v = new Variable(getCurrentText());
		advance();
		return v;
	}

	private String getCurrentText() {
		return getCurrentToken().getText();
	}

	private Expression parseConstantExpression() {
		Token token = getCurrentToken();
		advance();
		switch (token.getType()) {
		case Number:
			return new NumberParser().parse((NumberToken) token);
		case Character:
			return new CharacterConstant(token.getText());
		case String:
			return new StringConstant(token.getText());
		case Boolean:
			return new BooleanConstant(token.getText());
		default:
			throw new CouldNotParseConstant(token.getType());
		}
	}

	@SuppressWarnings("serial")
	private class CouldNotParseConstant extends RuntimeException {
		public TokenType actualType;

		public CouldNotParseConstant(TokenType type) {
			actualType = type;
		}
	}

	private Assignment parseSet() {
		consume(TokenType.OpeningBracket);
		consumeKeyword("set!");
		String id = consume(TokenType.Identifier).getText();
		Expression value = parseExpression();
		consume(TokenType.ClosingBracket);
		return new Assignment(id, value);
	}

	private Condition parseIfElse() {
		consume(TokenType.OpeningBracket);
		consumeKeyword("if");
		Expression condition = parseExpression();
		Expression ifClause = parseExpression();
		Expression elseClause = null;
		if (!is(TokenType.ClosingBracket))
			elseClause = parseExpression();
		consume(TokenType.ClosingBracket);
		return new Condition(condition, ifClause, elseClause);
	}

	private FunctionCall parseFunctionCall() {
		consume(TokenType.OpeningBracket);
		Expression callee = parseExpression();
		FunctionCall call = new FunctionCall(callee);
		while (!is(TokenType.ClosingBracket))
			call.getArguments().add(parseExpression());
		consume(TokenType.ClosingBracket);
		return call;
	}

	private Expression parseLambda() {
		consume(TokenType.OpeningBracket);
		consumeKeyword("lambda");
		Lambda lambda = new Lambda();
		if (is(TokenType.OpeningBracket))
			parseParameters(lambda);
		else
			lambda.setArgumentList(parseVariable());
		lambda.setBody(parseBody());
		consume(TokenType.ClosingBracket);
		return lambda;
	}

	private void parseParameters(Lambda lambda) {
		advance();
		while (!is(TokenType.ClosingBracket) && !is(TokenType.Dot))
			lambda.getArguments().add(parseVariable());
		if (is(TokenType.Dot)) {
			advance();
			lambda.setArgumentList(parseVariable());
		}
		consume(TokenType.ClosingBracket);
	}

	private Variable parseVariable() {
		return new Variable(consume(TokenType.Identifier).getText());
	}

	private Body parseBody() {
		Body body = new Body();
		while (!isExpressionStart())
			body.getDefinitions().add(parseDefinition());
		while (!is(TokenType.ClosingBracket))
			body.getExpressions().add(parseExpression());
		return body;
	}

	private Definition parseDefinition() {
		consume(TokenType.OpeningBracket);
		consumeKeyword("define");
		if (is(TokenType.OpeningBracket))
			return parseFunctionDefinition();
		else
			return parseVariableDefinition();
	}

	private Definition parseFunctionDefinition() {
		advance();
		FunctionDefinition def = new FunctionDefinition(parseVariable());
		parseAllParameters(def);
		parseArgumentListIfNecessary(def);
		consume(TokenType.ClosingBracket);
		def.setBody(parseBody());
		consume(TokenType.ClosingBracket);
		return def;
	}

	private void parseAllParameters(FunctionDefinition def) {
		while (!is(TokenType.ClosingBracket) && !is(TokenType.Dot))
			def.getParameters().add(parseVariable());
	}

	private void parseArgumentListIfNecessary(FunctionDefinition def) {
		if (is(TokenType.Dot)) {
			advance();
			def.setArgumentList(parseVariable());
		}
	}

	private Definition parseVariableDefinition() {
		Variable var = parseVariable();
		Expression value = parseExpression();
		consume(TokenType.ClosingBracket);
		return new VariableDefinition(var, value);
	}

}
